package mspbots.qbd.online.oauth;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.log4j.Log4j2;
import mspbots.qbd.common.RedisKeys;
import mspbots.qbd.online.annotation.ApiException;
import mspbots.qbd.online.core.client.ClientProperties;
import mspbots.qbd.online.utils.ClientUtils;
import org.springframework.data.redis.core.ReactiveRedisTemplate;
import org.springframework.expression.ExpressionException;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

import java.time.Duration;
import java.util.HashMap;
import java.util.Map;

/**
 * com.mspbots.doctor.response.AuthService
 *
 * @author Alex bob(https://github.com/vnobo)
 * @date Created by 2020/3/19
 */
@Log4j2
@Service
public class AuthService {

    private final WebClient webClient;
    private final ClientProperties booksProperties;
    private final ReactiveRedisTemplate<String, Object> reactiveRedisTemplate;
    private final ObjectMapper objectMapper;

    /**
     * https://oauth.platform.intuit.com/op/v1
     *
     * @param booksProperties       book properties config
     * @param builder               web build
     * @param reactiveRedisTemplate redis template
     * @param objectMapper          json mapper template
     */
    public AuthService(ClientProperties booksProperties,
                       WebClient.Builder builder,
                       ReactiveRedisTemplate<String, Object> reactiveRedisTemplate, ObjectMapper objectMapper) {
        this.booksProperties = booksProperties;
        this.webClient = builder.baseUrl("https://oauth.platform.intuit.com/")
                .defaultHeader("Accept", "application/json")
                .build();
        this.reactiveRedisTemplate = reactiveRedisTemplate;
        this.objectMapper = objectMapper;
    }

    public Mono<TokenState> getToken(Long tenantId) {

        Mono<TokenState> accessTokenMono = this.reactiveRedisTemplate.opsForValue()
                .get(RedisKeys.ONLINE_ACCESS_TOKEN_REDIS_KEY + tenantId)
                .map(result -> this.objectMapper.convertValue(result, TokenState.class));

        return accessTokenMono.switchIfEmpty(this.loadAuthorizationCode(tenantId)
                .flatMap(authorizationCode -> this.refreshToken(tenantId, authorizationCode))
                .map(authorizationCode -> TokenState.withDefault(tenantId).setAccessToken(authorizationCode.getAccessToken())
                        .setRealmId(authorizationCode.getRealmId())));

    }

    private Mono<AuthorizationCode> loadAuthorizationCode(Long tenantId) {
        return this.reactiveRedisTemplate.opsForValue()
                .get(RedisKeys.ONLINE_AUTHORIZE_REDIS_KEY + tenantId)
                .switchIfEmpty(Mono.defer(() ->
                        Mono.error(new ExpressionException("Your access token is empty,Allow access your account."))))
                .map(result -> this.objectMapper.convertValue(result, AuthorizationCode.class));
    }

    public Mono<AuthorizationCode> authToken(Map<String, Object> params) {
        params.put("grant_type", "authorization_code");
        params.put("redirect_uri", this.booksProperties.getRedirectUri());
        return this.tokenClientGet(Long.parseLong(params.get("state").toString()), params);
    }

    protected Mono<AuthorizationCode> refreshToken(Long tenantId, AuthorizationCode authorizationCode) {
        Map<String, Object> params = new HashMap<>(6);
        params.put("grant_type", "refresh_token");
        params.put("refresh_token", authorizationCode.getRefreshToken());
        params.put("realmId", authorizationCode.getRealmId());
        return this.tokenClientGet(tenantId, params);
    }

    private Mono<AuthorizationCode> tokenClientGet(Long tenantId, Map<String, Object> params) {

        log.debug("Get token params {}", params);

        return this.webClient.post().uri(uriBuilder -> uriBuilder.path("/oauth2/v1/tokens/bearer").build())
                .headers(headers -> headers.setBasicAuth(booksProperties.getClientId(), booksProperties.getSecretKey()))
                .headers(headers -> headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED))
                .bodyValue(ClientUtils.form(params))
                .retrieve()
                .onStatus(HttpStatus::isError, clientResponse -> clientResponse.bodyToMono(JsonNode.class)
                        .map(errNode -> ApiException.withMsg(clientResponse.statusCode(), errNode)))
                .bodyToMono(JsonNode.class)
                .map(jsonNode -> AuthorizationCode.builder().accessToken(jsonNode.get("access_token").textValue())
                        .realmId(params.get("realmId").toString())
                        .refreshToken(jsonNode.get("refresh_token").textValue())
                        .expiresIn(jsonNode.get("expires_in").longValue())
                        .refreshTokenExpiresIn(jsonNode.get("x_refresh_token_expires_in").longValue())
                        .tokenType(jsonNode.get("token_type").textValue())
                        .build())
                .doOnNext(authCode -> this.reactiveRedisTemplate.opsForValue()
                        .set(RedisKeys.ONLINE_AUTHORIZE_REDIS_KEY + tenantId,
                                authCode, Duration.ofSeconds(authCode.getRefreshTokenExpiresIn()))
                        .flatMap(v -> this.reactiveRedisTemplate.opsForValue()
                                .set(RedisKeys.ONLINE_ACCESS_TOKEN_REDIS_KEY + tenantId, TokenState.withDefault(tenantId)
                                                .setAccessToken(authCode.getAccessToken()).setRealmId(authCode.getRealmId()),
                                        Duration.ofSeconds(authCode.getExpiresIn())))
                        .subscribe(result -> log.debug("Redis cache save token success.")));
    }

}
